import tkinter as tk
import locale
from tkinter import font as tkFont

class DebouncedNumericEntry(tk.Entry):
    def __init__(self, master=None, debounce_delay=500, warning_duration=3000, allow_decimal=True, allow_negative=True, **kwargs):
        """
        初始化数字输入框组件
        :param master: 父组件
        :param debounce_delay: 防抖延迟时间（毫秒）
        :param warning_duration: 警告标签显示时长（毫秒）
        :param allow_decimal: 是否允许小数点
        :param allow_negative: 是否允许负号
        :param kwargs: 其他传递给Entry的参数
        """
        super().__init__(master, **kwargs)
        self.debounce_delay = debounce_delay  # 防抖延迟时间
        self.warning_duration = warning_duration  # 警告标签显示时长
        self.allow_decimal = allow_decimal  # 是否允许小数点
        self.allow_negative = allow_negative  # 是否允许负号
        self.after_id = None  # 用于存储after方法的ID

        # 如果有textvariable, 初始化时检查并清理
        if "textvariable" in kwargs:
            self.textvariable = kwargs["textvariable"]
            self._check_and_clean()
            # 绑定textvariable的trace方法, 监听变量变化
            self.textvariable.trace_add("write", self._on_textvariable_change)

        # 绑定事件
        self.bind("<KeyRelease>", self._on_key_release)
        self.bind("<Configure>", self._on_resize)

        self.default_font = tkFont.nametofont("TkDefaultFont")
        self.warning_label = tkFont.Font(family=self.default_font["family"], size=self.default_font["size"] - 2, weight=self.default_font['weight'])
        self.warning_label = tk.Label(self, text="Only numbers, dot or minus!", font=self.warning_label)
        self.warning_label.config(bg="yellow", fg="red")
        # 根据allow_decimal和allow_negative动态设置警告信息
        if locale.getlocale()[0].startswith('Chinese'):
            if allow_decimal and allow_negative:
                self.warning_label.config(text='仅支持数字、\n小数点或负号')
            elif allow_decimal:
                self.warning_label.config(text='仅支持数字或小数点')
            elif allow_negative:
                self.warning_label.config(text='仅支持数字或负号')
            else:
                self.warning_label.config(text='仅支持数字')
        else:
            if allow_decimal and allow_negative:
                self.warning_label.config(text="Only numbers, dot or minus!")
            elif allow_decimal:
                self.warning_label.config(text="Only numbers or dot!")
            elif allow_negative:
                self.warning_label.config(text="Only numbers or minus!")
            else:
                self.warning_label.config(text="Only numbers!")

    def _on_textvariable_change(self, *args):
        """
        textvariable变化时触发的处理函数
        """
        # 触发防抖逻辑
        self._on_key_release()

    def _on_key_release(self, event=None):
        """
        键盘释放事件处理函数
        """
        # 如果之前有延迟任务, 则取消它
        if self.after_id is not None:
            self.after_cancel(self.after_id)
        
        # 设置新的延迟任务
        self.after_id = self.after(self.debounce_delay, self._check_and_clean)

    def _check_and_clean(self):
        """
        检查输入内容并清理非法字符
        """
        # 获取当前输入框内容
        content = self.get()
        # 获取当前光标位置
        current_cursor_pos = self.index(tk.INSERT)
        # 获取光标前的字符是数字的数量
        bfc = ''.join(filter(self.is_valid_char, content[:current_cursor_pos]))
        p = len(self.clean_content(bfc))
        # 过滤非数字部分
        filtered_content = ''.join(filter(self.is_valid_char, content))
        filtered_content = self.clean_content(filtered_content)
        # 如果过滤后的内容与原始内容不同, 则更新输入框
        if filtered_content != content:
            # 清空+插入=更新
            self.delete(0, tk.END)
            self.insert(0, filtered_content)
            # 更新textvariable
            if hasattr(self, 'textvariable'):
                self.textvariable.set(filtered_content)
            # 更新光标位置
            self.icursor(p)
            # 显示提示
            self._flash_warning_label()

        # 清空after_id
        self.after_id = None

    def _flash_warning_label(self):
        """短暂显示警告标签并自动隐藏"""
        if hasattr(self, 'warning_label'):
            self.warning_label.place(x=self.winfo_width() - self.warning_label.winfo_width(), y=0)  # 组件未完全渲染可能会得到0, 直接就用0就行了, 无所谓
            self.warning_label.after(self.warning_duration, lambda: self.warning_label.place_forget())

    def _on_resize(self, event=None):
        self._update_warning_label_position()

    def _update_warning_label_position(self):
        if self.warning_label.winfo_ismapped():
            self.warning_label.place_forget()
            self.warning_label.place(x=self.winfo_width() - self.warning_label.winfo_width(), y=0)
            self.warning_label.after(self.warning_duration, lambda: self.warning_label.place_forget())

    def is_valid_char(self, char: str):
        """检查字符是否为数字、小数点或负号"""
        if char.isdigit():
            return True
        if self.allow_decimal and char == '.':
            return True
        if self.allow_negative and char == '-':
            return True
        return False

    def clean_content(self, content: str):
        """
        清理多余的负号和小数点
        - 负号只允许在首位
        - 多个小数点只保留最左侧的小数点
        """
        # 如果内容为空, 直接返回
        if not content:
            return content

        # 检查负号
        if self.allow_negative and content[0] == '-':
            content = '-' + content[1:].replace('-', '')
        else:
            content = content.replace('-', '')

        # 检查小数点
        if self.allow_decimal and '.' in content:
            parts = content.split('.')
            content = parts[0] + '.' + ''.join(parts[1:])  # 只保留最左侧的小数点
        else:
            content = content.replace('.', '')  # 去掉所有小数点

        return content


# 测试代码
if __name__ == "__main__":
    root = tk.Tk()
    root.geometry("400x200+400+400")
    root.title("防抖数字输入框")

    text = tk.StringVar(value='默认值是1')
    entry = DebouncedNumericEntry(root, debounce_delay=500, warning_duration=500, textvariable=text)
    entry.pack()

    root.after(1000, lambda: entry.config(width=50))
    root.after(2000, lambda: text.set("修改为2"))

    root.mainloop()
